package org.springframework.roo.addon.graph;

import static java.lang.reflect.Modifier.PUBLIC;
import static java.util.Arrays.asList;
import static org.springframework.roo.addon.graph.support.MetaDataFactory.*;
import static org.springframework.roo.addon.graph.support.MethodBuilder.method;
import static org.springframework.roo.addon.graph.support.Tuple2._;
import static org.springframework.roo.model.JavaType.LONG_OBJECT;
import static org.springframework.roo.model.JavaType.LONG_PRIMITIVE;

import java.io.File;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;

import org.springframework.roo.addon.graph.support.BuildingItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils;
import org.springframework.roo.classpath.PhysicalTypeMetadata;
import org.springframework.roo.classpath.details.*;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder;
import org.springframework.roo.classpath.operations.ClasspathOperations;
import org.springframework.roo.metadata.MetadataIdentificationUtils;
import org.springframework.roo.model.ImportRegistrationResolver;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.project.Path;

import javax.annotation.Resource;

public abstract class GraphMetadata extends
        BuildingItdTypeDetailsProvidingMetadataItem
{
    private static final String PROVIDES_TYPE_STRING = GraphMetadata.class.getName();
    static final String PROVIDES_TYPE = MetadataIdentificationUtils.create( PROVIDES_TYPE_STRING );
    private final Type type;
    protected final ClasspathOperations classpathOperations;

    private GraphMetadata(final String identifier, final JavaType aspectName,
            final PhysicalTypeMetadata governorPhysicalTypeMetadata,
            final ClassOrInterfaceTypeDetails governorTypeDetails,
            final Type type, final ClasspathOperations classpathOperations)
    {
        super( identifier, aspectName, governorTypeDetails, governorPhysicalTypeMetadata );
        this.type = type;
        this.classpathOperations = classpathOperations;
        buildDetails();
        this.itdTypeDetails=builder.build();
    }

    public JavaType getJavaType()
    {
        return governorTypeDetails.getName();
    }

    public File getSourceFile()
    {
        return new File(
                governorPhysicalTypeMetadata.getPhysicalLocationCanonicalPath() );
    }

    public boolean isRelationship()
    {
        return type==Type.RELATIONSHIP;
    }

    public Iterable<Void> properties()
    {
        return Arrays.<Void>asList();
    }

    public Iterable<Void> relatedEntities()
    {
        return Arrays.<Void>asList();
    }

    static GraphMetadata node(final GraphDatabaseProvider graphdbProvider,
                              final String identifier, final JavaType aspectName,
                              final PhysicalTypeMetadata governorPhysicalTypeMetadata,
                              final ClassOrInterfaceTypeDetails governorTypeDetails, final ClasspathOperations classpathOperations)
    {
        return new GraphMetadata(identifier, aspectName,
                governorPhysicalTypeMetadata, governorTypeDetails,
                Type.NODE,classpathOperations) {
        	@Override
            public void buildDetails() {
        		final JavaType target = getJavaType();
                final String targetClass = target.getSimpleTypeName();
        		final ImportRegistrationResolver importResolver = builder.getImportRegistrationResolver();
				importResolver.addImport(Type.Types.Finder);
        		importResolver.addImport(Type.Types.FinderFactory);
        		importResolver.addImport(target);

                // todo indexed field
                for (final FieldMetadata fieldMetadata : governorTypeDetails.getDeclaredFields()) {
                    final List<AnnotationMetadata> annotations = fieldMetadata.getAnnotations();
                    for (final AnnotationMetadata annotation : annotations) {
                        if (annotation.getAnnotationType().equals(Type.Annotations.RelatedToViaAnnotation)) {
                            addRelationshipMethods(builder, annotation);
                        }
                    }
                }
        		// todo add finder as field, initialized by factory
        		builder.addField(new FieldMetadataBuilder(getId(), Modifier.PRIVATE,asList(annotationBuilder(Resource.class)), symbol("finderFactory"), Type.Types.FinderFactory));
                final ConstructorMetadataBuilder constructor = new ConstructorMetadataBuilder(getId());
                constructor.setModifier(Modifier.PUBLIC);
                constructor.setParameterTypes(AnnotatedJavaType.convertFromJavaTypes(asList(Type.Types.Node)));
                constructor.setParameterNames(asList(symbol("n")));
                constructor.getBodyBuilder().appendFormalLine("setUnderlyingState(n);");
                builder.addConstructor(constructor);

                method("findById", _(LONG_OBJECT,symbol("id"))).returns(getJavaType()).line(
                    "return finderFactory.getFinderForClass(%s.class).findById(:param1);", targetClass
                ).add(getId(),builder);
				method("count").returns(LONG_PRIMITIVE).line(
                    "return finderFactory.getFinderForClass(%s.class).count();", targetClass
                ).add(getId(), builder);
                method("findAll").returns(collectionType(Iterable.class, target)).line(
                    "return finderFactory.getFinderForClass(%s.class).findAll();", targetClass
                ).add(getId(),builder);
        	}

            private void addRelationshipMethods(final ItdTypeDetailsBuilder builder, final AnnotationMetadata annotation) {
                final JavaType relationshipEntity = (JavaType) annotation.getAttribute(symbol("elementClass")).getValue();
                final ClassOrInterfaceTypeDetails relationshipType = classpathOperations.getClassOrInterface(relationshipEntity);
                final JavaType targetNode=getOtherNodeType(relationshipType);
                final JavaSymbolName targetName = symbol(targetNode);
                final String relationshipTypeName = (String) annotation.getAttribute(symbol("type")).getValue();
                final String relationshipEntityName = relationshipEntity.getSimpleTypeName();

                method("get" + relationshipEntityName,_(targetNode, targetName)).returns(relationshipEntity).line(
                   "return (%1$s)getRelationshipTo(%2$s,%1$s.class,\"%3$s\");", relationshipEntityName, targetName, relationshipTypeName
                ).add(getId(),builder);
                method("relateTo" + targetNode.getSimpleTypeName(),_(targetNode, targetName)).returns(relationshipEntity).line(
                   "return (:return)relateTo(:param1,:return.class,\"%s\");",relationshipTypeName
                ).add(getId(), builder);
            }
            private JavaType getOtherNodeType(final ClassOrInterfaceTypeDetails relationshipType) {
                for (final FieldMetadata fieldMetadata : relationshipType.getDeclaredFields()) {
                    for (final AnnotationMetadata annotationMetadata : fieldMetadata.getAnnotations()) {
                        if (annotationMetadata.getAnnotationType().equals(Type.Annotations.StartNode) || annotationMetadata.getAnnotationType().equals(Type.Annotations.EndNode)) {
                            if (!fieldMetadata.getFieldType().equals(getJavaType())) return fieldMetadata.getFieldType();
                        }
                    }
                }
                throw new IllegalStateException("Other NodeEntity to "+getJavaType()+" not found in relationship "+relationshipType);
            }
        };
    }


    static GraphMetadata relationship(final GraphDatabaseProvider graphdbProvider,
                                      final String identifier, final JavaType aspectName,
                                      final PhysicalTypeMetadata governorPhysicalTypeMetadata,
                                      final ClassOrInterfaceTypeDetails governorTypeDetails, final ClasspathOperations classpathOperations)
    {
        return new GraphMetadata(identifier, aspectName,
                governorPhysicalTypeMetadata, governorTypeDetails,
                Type.RELATIONSHIP,classpathOperations) {
        	@Override
            public void buildDetails() {
                //builder.addTypeAnnotation(Type.RELATIONSHIP.getGraphAnnotation());
                final ConstructorMetadataBuilder constructor = new ConstructorMetadataBuilder(getId());
                constructor.setModifier(PUBLIC);
                constructor.addParameterType(new AnnotatedJavaType(type("org.neo4j.graphdb.Relationship"),null));
                constructor.addParameterName(symbol("r"));
                constructor.setBodyBuilder(new InvocableMemberBodyBuilder().appendFormalLine("setUnderlyingState(r);"));
                builder.addConstructor(constructor.build());
        	}
        };
    }


    static String createIdentifier( final JavaType javaType, final Path path )
    {
        return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path );
    }
    
    static JavaType getJavaType( final String metadataIdentificationString )
    {
        return PhysicalTypeIdentifierNamingUtils.getJavaType(
                PROVIDES_TYPE_STRING, metadataIdentificationString );
    }

    static final Path getPath( final String metadataIdentificationString )
    {
        return PhysicalTypeIdentifierNamingUtils.getPath( PROVIDES_TYPE_STRING,
                metadataIdentificationString );
    }
}
